﻿'
' Boogaloo® - http://www.mindfly.com
' Copyright (c) 2008
' by Mindfly, Inc.
'
' Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 
' documentation files (the "Software"), to deal in the Software without restriction, including without limitation 
' the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 
' to permit persons to whom the Software is furnished to do so, subject to the following conditions:
'
' The above copyright notice and this permission notice shall be included in all copies or substantial portions 
' of the Software.
'
' THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
' INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
' DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
' SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
' SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
' WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
' THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Imports System.IO
Imports System.Xml
Imports System.Web
Imports System.Text
Imports Boogaloo.Core.DAL

Namespace BLL


    Public Class ThemeCSSManifest
        Inherits ThemeBase
        Implements ICSSManifest

#Region "Constructors"



        Public Sub New(ByVal theme As String)
            Me.theme = theme

            ' think the error is in GetManifestXml ... cache is purged thus returns nothing
            Dim manifest As XmlDocument = GetManifestXml()
            If manifest IsNot Nothing Then

                Try
                    With manifest.DocumentElement

                        Me.bundle = CType(.GetAttribute("bundle"), Boolean)
                        Me.removewhitespace = CType(.GetAttribute("removewhitespace"), Boolean)
                        Me.compress = CType(.GetAttribute("compress"), Boolean)

                        Dim groups As XmlNodeList = .SelectNodes("group")
                        Dim group As XmlElement = Nothing

                        For Each group In groups

                            Dim cssfiles As XmlNodeList = group.SelectNodes("stylesheet")
                            Dim css As XmlNode = Nothing
                            For Each css In cssfiles
                                Dim stylesheet As ThemeCSSStyleSheet = Nothing
                                Dim elcss As XmlElement = CType(css, XmlElement)
                                If Not String.IsNullOrEmpty(elcss.GetAttribute("media")) Then
                                    stylesheet = New ThemeCSSStyleSheet(elcss.GetAttribute("href"), elcss.GetAttribute("media"))
                                Else
                                    stylesheet = New ThemeCSSStyleSheet(elcss.GetAttribute("href"))
                                End If

                                Dim key As String = group.GetAttribute("test")

                                If Not stylesheets.ContainsKey(key) Then stylesheets.Add(key, New List(Of ThemeCSSStyleSheet))
                                stylesheets(key).Add(stylesheet)

                            Next

                        Next

                        _state = content_state.loaded


                    End With


                Catch ex As Exception
                    ' mark state as changed which will force a save during validation
                    _state = content_state.changed
                End Try

            Else

                _state = content_state.changed

            End If

            Validate()

        End Sub

        Public Sub New(ByVal theme As String, ByVal bundle As Boolean, _
                       ByVal compress As Boolean, ByVal removewhitespace As Boolean, ByVal stylesheets As Dictionary(Of String, List(Of ThemeCSSStyleSheet)))

            Me.theme = theme
            Me.bundle = bundle
            Me.compress = compress
            Me.removewhitespace = removewhitespace
            Me._stylesheets = stylesheets

            _state = content_state.changed

            Validate()
        End Sub



#End Region


        Public Shared Function GetThemeCSSManifest(ByVal theme_name As String) As ThemeCSSManifest
            Return New ThemeCSSManifest(theme_name)
        End Function


        Public Function GetStyleSheetCollection() As List(Of ThemeCSSStyleSheet)
            Dim styles As New List(Of ThemeCSSStyleSheet)
            Dim group As KeyValuePair(Of String, List(Of ThemeCSSStyleSheet)) = Nothing
            For Each group In stylesheets
                styles.AddRange(group.Value)
            Next
            Return styles
        End Function

        Private Sub Validate()

            ' Assert that the nodisplay dictionary is present in case additional files have been added
            If Not stylesheets.ContainsKey("none") Then stylesheets.Add("none", New List(Of ThemeCSSStyleSheet))

            Dim actual_css_files As String() = GetCssFileNamesFromThemesDirectory()
            Dim cssfile As String = String.Empty

            For Each cssfile In actual_css_files
                If Not Me.Contains(cssfile) Then
                    stylesheets("none").Add(New ThemeCSSStyleSheet(String.Format("{0}{1}", path_theme, cssfile.Substring(cssfile.LastIndexOf("\") + 1))))
                    _state = content_state.changed
                End If
            Next

            If _state = content_state.changed Then SaveManifest()

            _validated = True

        End Sub


        Private Sub AddGroup(ByVal group_key As String)
            If Not stylesheets.ContainsKey(group_key) Then
                stylesheets.Add(group_key, New List(Of ThemeCSSStyleSheet))
                _state = content_state.changed
            End If

        End Sub





        ''' <summary>
        ''' Returns a list of all css file names in the ASP.NET theme directory
        ''' </summary>
        Private Function GetCssFileNamesFromThemesDirectory() As String()
            Return Directory.GetFiles(context.Server.MapPath(path_theme), "*.css")
        End Function


        ''' <summary>
        ''' Searchs the manifest stylesheet dictionary lists for a css file of name provided.
        ''' cssfile is simply the name of the file ex. style.css
        ''' </summary>
        Public Function Contains(ByVal csspath As String) As Boolean Implements ICSSManifest.Contains
            Dim found As Boolean = False
            Dim keyvalue As KeyValuePair(Of String, List(Of ThemeCSSStyleSheet)) = Nothing
            For Each keyvalue In stylesheets
                found = ContainerContainsCssFile(keyvalue, csspath)
                If found Then Exit For
            Next
            Return found
        End Function

        Private Function ContainerContainsCssFile(ByVal keyvalue As KeyValuePair(Of String, List(Of ThemeCSSStyleSheet)), ByVal csspath As String) As Boolean
            Dim found As Boolean = False
            Dim i As Integer = 0
            While i < keyvalue.Value.Count And Not found
                Dim cssfile As String = keyvalue.Value(i).path

                If String.Equals(cssfile, csspath) Then
                    found = True
                Else
                    i = i + 1
                End If

            End While
            Return found
        End Function

        Private Function GetCSSStyleSheetByCssFileName(ByVal csspath As String) As ThemeCSSStyleSheet
            Dim list As List(Of ThemeCSSStyleSheet) = Nothing
            Dim css As ThemeCSSStyleSheet = Nothing
            Dim i As Integer = 0
            For Each list In stylesheets.Values
                Dim check As ThemeCSSStyleSheet = Nothing
                For Each check In list
                    If check.path = csspath Then
                        css = check
                        Exit For
                    End If
                Next
                If css IsNot Nothing Then Exit For
            Next
            Return css
        End Function

        Public Function GetContainerByCssFileName(ByVal csspath As String) As KeyValuePair(Of String, List(Of ThemeCSSStyleSheet))

            Dim found As Boolean = False
            Dim keyvalue As KeyValuePair(Of String, List(Of ThemeCSSStyleSheet)) = Nothing
            For Each keyvalue In stylesheets
                found = ContainerContainsCssFile(keyvalue, csspath)
                If found Then Exit For
            Next
            If found Then
                Return keyvalue
            Else
                Return Nothing
            End If

        End Function



        Public Function UpdateStyleSheetMedia(ByVal csspath As String, ByVal media As String) As Boolean
            Dim found As Boolean = False
            Dim css As ThemeCSSStyleSheet = GetCSSStyleSheetByCssFileName(csspath)
            If css IsNot Nothing Then
                found = True
                css.media = media
                _state = content_state.changed
            End If

            If _state = content_state.changed Then SaveManifest()

            Return found
        End Function

        Public Function SetStyleSheetGroup(ByVal csspath As String, ByVal group_key As String) As Boolean

            Dim container As KeyValuePair(Of String, List(Of ThemeCSSStyleSheet)) = GetContainerByCssFileName(csspath)

            If container.Key IsNot Nothing Then


                If Not String.Equals(container.Key, group_key) Then

                    ' remove the item from this list
                    Dim css As ThemeCSSStyleSheet = Nothing
                    For Each css In container.Value
                        If css.path = csspath Then
                            container.Value.Remove(css)
                            _state = content_state.changed
                            Exit For
                        End If
                    Next

                    If _state = content_state.changed Then

                        If Not stylesheets.ContainsKey(group_key) Then stylesheets.Add(group_key, New List(Of ThemeCSSStyleSheet))
                        stylesheets(group_key).Add(css)

                        SaveManifest()

                    End If

                End If

                Return True
            Else
                Return False
            End If


        End Function

        ''' <summary>
        ''' Returns a string containing the new header link tag construct
        ''' HtmlLink(s) will either be directly to the actual css file (only ordered) or
        ''' to the css.axd?theme={0}&amp;cond={1} HttpHandler if the theme is to be bundled
        ''' </summary>
        Function GetHeaderHtml() As String Implements ICSSManifest.GetHeaderHtml

            Dim HtmlLinks As String = String.Empty
            
            Dim group As KeyValuePair(Of String, List(Of ThemeCSSStyleSheet)) = Nothing
            Dim sb As New StringBuilder
            sb.Append(vbCrLf)

            For Each group In stylesheets

                If Not String.Equals(group.Key, "nodisplay") Then



                    If Not String.Equals(group.Key, "none") Then
                        sb.Append(String.Format("<!--{0}>{1}", group.Key, vbCrLf))
                    End If
                    If bundle Then

                        ' only include the bundle call if there are stylesheets that can be grouped
                        ' this avoids the reference to a linked stylesheet with no data
                        If group.Value.Count > 1 Or (group.Value.Count = 1 And String.IsNullOrEmpty(group.Value(0).media)) Then
                            sb.Append(GetHtmlLinkTag(String.Format("{0}css.axd?theme={1}&amp;cond={2}", path_theme, theme, GetQueryStringGroupKey(group.Key))) & vbCrLf)
                        End If

                        ' check for stylesheets with specific media attributes
                        Dim css As ThemeCSSStyleSheet = Nothing
                        For Each css In group.Value
                            If Not String.IsNullOrEmpty(css.media) Then
                                If removewhitespace Or compress Then
                                    sb.Append(GetHtmlLinkTag(String.Format("{0}css.axd?theme={1}&amp;stylesheet={2}", path_theme, theme, HttpUtility.UrlEncode(css.href)), css.media) & vbCrLf)
                                Else
                                    sb.Append(GetHtmlLinkTag(css.href, css.media) & vbCrLf)
                                End If
                            End If
                        Next
                    Else

                        Dim css As ThemeCSSStyleSheet = Nothing
                        For Each css In group.Value

                            If removewhitespace Or compress Then
                                sb.Append(GetHtmlLinkTag(String.Format("{0}css.axd?theme={1}&amp;stylesheet={2}", path_theme, theme, HttpUtility.UrlEncode(css.href)), css.media) & vbCrLf)
                            Else
                                sb.Append(GetHtmlLinkTag(css.href, css.media) & vbCrLf)
                            End If

                        Next
                    End If
                    If Not String.Equals(group.Key, "none") Then
                        sb.Append(String.Format("{0}{1}", "<![endif]-->", vbCrLf))
                    End If

                End If

            Next
            sb.Append(vbCrLf)

            HtmlLinks = sb.ToString

           
            Return HtmlLinks
            
        End Function

        ''' <summary>
        ''' Creates a link tag string
        ''' </summary>
        Private Overloads Function GetHtmlLinkTag(ByVal href As String) As String
            Dim vpath As String = context.Request.ApplicationPath
            If vpath = "/" Then vpath = String.Empty
            Return String.Format("<link rel=""stylesheet"" type=""text/css"" href=""{0}"" />", GetVirtualPath(href))
        End Function

        ''' <summary>
        ''' Creates a link tag string with a media attribute
        ''' </summary>
        Private Overloads Function GetHtmlLinkTag(ByVal href As String, ByVal media As String) As String
            If Not String.IsNullOrEmpty(media) Then
                Return String.Format("<link rel=""stylesheet"" type=""text/css"" href=""{0}"" media=""{1}"" />", GetVirtualPath(href), media)
            Else
                Return GetHtmlLinkTag(href)
            End If
        End Function

        ''' <summary>
        ''' Sets the virtual path to the stylesheet
        ''' </summary>
        Private Function GetVirtualPath(ByVal href As String) As String
            Dim vpath As String = context.Request.ApplicationPath
            If vpath = "/" Then vpath = String.Empty
            Return href.Replace("~", vpath)
        End Function


        ''' <summary>
        ''' Combines the contents of each stylesheet into a single string. The qs_key arguement is the actual conditional comment test
        ''' with spaces and brackets removed
        ''' </summary>
        Function GetConditionStylesheetGroupContentsByQueryStringGroupKey(ByVal qs_key As String) As String Implements ICSSManifest.GetConditionStylesheetGroupContentsByQueryStringGroupKey

            Dim group_contents As String = String.Empty

            Dim contents As List(Of ThemeCSSStyleSheet) = GetConditionByQueryStringGroupKey(qs_key)
            Dim sb As New StringBuilder
            Dim css As ThemeCSSStyleSheet = Nothing
            For Each css In contents
                If String.IsNullOrEmpty(css.media) Then
                    sb.Append("/*" & vbCrLf)
                    sb.Append("CSS for : " & css.href)
                    sb.Append("*/" & vbCrLf)
                    sb.Append(css.contents)
                    sb.Append(vbCrLf)
                End If
            Next

            group_contents = sb.ToString

            Return group_contents
        End Function



        ''' <summary>
        ''' Returns a list of ThemeCSSStyleSheet from a condition block.  The key arguement is the actual conditional comment test
        ''' with spaces and brackets removed
        ''' </summary>
        Public Function GetConditionByQueryStringGroupKey(ByVal key As String) As List(Of ThemeCSSStyleSheet) Implements ICSSManifest.GetConditionByQueryStringGroupKey
            Dim found As List(Of ThemeCSSStyleSheet) = Nothing
            Dim group As KeyValuePair(Of String, List(Of ThemeCSSStyleSheet)) = Nothing

            For Each group In stylesheets
                If String.Equals(GetQueryStringGroupKey(group.Key), key) Then
                    found = group.Value
                    Exit For
                End If
            Next
            If found Is Nothing Then found = New List(Of ThemeCSSStyleSheet)
            Return found
        End Function


        ''' <summary>
        ''' Retrieves the manifest xml file from the ASP.NET theme folder 
        ''' as an XmlDocument if it exists
        ''' </summary>
        Private Function GetManifestXml() As XmlDocument
            Dim key As String = "theme_theme_manifestxml_" & theme

            Dim manifest As XmlDocument = Nothing

            If ThemeBase.Cache(key) IsNot Nothing And ThemeBase.ProviderSettings.EnableCaching Then
                manifest = CType(ThemeBase.Cache(key), XmlDocument)
            Else
                If File.Exists(Me.path) Then

                    manifest = New XmlDocument
                    manifest.Load(Me.path)

                    ThemeBase.Cache.Insert(key, manifest, New Caching.CacheDependency(path))

                End If
            End If


            Return manifest
        End Function


        Public Function SaveManifest() As Boolean
            Try
                Dim sb As New StringBuilder
                Using writer As XmlWriter = XmlWriter.Create(sb)

                    writer.WriteStartDocument()
                    writer.WriteStartElement("stylesheets")
                    writer.WriteAttributeString("theme", Me.theme)
                    writer.WriteAttributeString("bundle", Me.bundle.ToString)
                    writer.WriteAttributeString("compress", Me.compress.ToString)
                    writer.WriteAttributeString("removewhitespace", Me.removewhitespace.ToString)

                    Dim conditional As KeyValuePair(Of String, List(Of ThemeCSSStyleSheet)) = Nothing
                    For Each conditional In stylesheets

                        If conditional.Value.Count > 0 Then

                            writer.WriteStartElement("group")
                            writer.WriteAttributeString("test", conditional.Key)
                            writer.WriteAttributeString("key", GetQueryStringGroupKey(conditional.Key))
                            Dim css As ThemeCSSStyleSheet = Nothing
                            For Each css In conditional.Value
                                writer.WriteStartElement("stylesheet")
                                writer.WriteAttributeString("href", css.href)
                                If Not String.IsNullOrEmpty(css.media) Then writer.WriteAttributeString("media", context.Server.HtmlEncode(css.media))
                                writer.WriteEndElement() ' stylesheet
                            Next
                            writer.WriteEndElement() ' group

                        End If

                    Next


                    writer.WriteEndElement() ' stylesheets
                    writer.WriteEndDocument()

                End Using


                ' hack to save to document to get around problems with loading on subsequent requests
                Dim doc As New XmlDocument
                doc.LoadXml(sb.ToString)
                doc.Save(path)

                Return True
            Catch ex As Exception
                Return False
            End Try

        End Function



        ''' <summary>
        ''' Returns the test without brackets and spaces in the lower variant for more appropriate querystring referneces
        ''' </summary>
        Private Function GetQueryStringGroupKey(ByVal group_key As String) As String
            Return group_key.Replace("[", String.Empty).Replace("]", String.Empty).Replace(" ", String.Empty).ToLower
        End Function

#Region "Properties"


        ''' <summary>
        ''' ASP.NET theme folder /App_Theme/{0} for which this object creates the manifest
        ''' </summary>
        Private _theme As String = String.Empty
        Public Property theme() As String Implements ICSSManifest.theme
            Get
                Return _theme
            End Get
            Set(ByVal value As String)
                _theme = value
            End Set
        End Property

        ''' <summary>
        ''' Indicates whether or not the ASP.NET Theme's css style sheets should be bundled 
        ''' into a single request (CSSHandler - css.axd)
        ''' </summary>
        Private _bundle As Boolean = False
        Public Property bundle() As Boolean Implements ICSSManifest.bundle
            Get
                Return _bundle
            End Get
            Set(ByVal value As Boolean)
                _bundle = value
            End Set
        End Property

        ''' <summary>
        ''' Indicates whether or not the CSSHandler should attempt to compress the stylesheets
        ''' </summary>
        Private _compress As Boolean = False
        Public Property compress() As Boolean Implements ICSSManifest.compress
            Get
                Return _compress
            End Get
            Set(ByVal value As Boolean)
                _compress = value
            End Set
        End Property


        ''' <summary>
        ''' Indicates whether or not the CSSHandler should remove whitespace prior to writting
        ''' to response
        ''' </summary>
        Private _removewhitespace As Boolean = False
        Public Property removewhitespace() As Boolean Implements ICSSManifest.removewhitespace
            Get
                Return _removewhitespace
            End Get
            Set(ByVal value As Boolean)
                _removewhitespace = value
            End Set
        End Property

        ''' <summary>
        ''' Dictionary of list of stylesheets described in the manifest xml file
        ''' String keys indicate the conditional comment grouping with 'none' used as
        ''' standard link.  
        ''' </summary>
        Private _stylesheets As New Dictionary(Of String, List(Of ThemeCSSStyleSheet))
        Public ReadOnly Property stylesheets() As System.Collections.Generic.Dictionary(Of String, System.Collections.Generic.List(Of ThemeCSSStyleSheet)) Implements ICSSManifest.stylesheets
            Get
                Return _stylesheets
            End Get
        End Property

        ''' <summary>
        ''' Indicates whether or not the manifest has been validated against
        ''' the actual ASP.NET directory structure to assert css files exist or
        ''' new ones have not been added.
        ''' </summary>
        Private _validated As Boolean = False
        Public ReadOnly Property validated() As Boolean Implements ICSSManifest.validated
            Get
                Return _validated
            End Get
        End Property

        ''' <summary>
        ''' Physical path to where the manifest file should be if it exists
        ''' </summary>
        Public ReadOnly Property path() As String Implements ICSSManifest.path
            Get
                Return context.Server.MapPath(String.Format("{0}{1}_theme.manifest", path_manifest, theme))
            End Get
        End Property


        ''' <summary>
        ''' Physical path to the App_Theme directory
        ''' </summary>
        Public ReadOnly Property path_theme() As String Implements ICSSManifest.path_theme
            Get
                Return String.Format("~/App_Themes/{0}/", theme)
                'Return "~/"
            End Get
        End Property


        Public ReadOnly Property path_manifest() As String Implements ICSSManifest.path_manifest
            Get
                Return String.Format(CoreSettings.Settings("THEME_ManifestDirectory").Value)
            End Get
        End Property

        ''' <summary>
        ''' HttpContext.Current
        ''' </summary>
        Private ReadOnly Property context() As HttpContext
            Get
                Return HttpContext.Current
            End Get
        End Property


        ''' <summary>
        ''' Enumeration indicating the state of the information stored in this instantiation
        ''' </summary>
        Private _state As content_state = content_state.empty

        Private Enum content_state
            empty
            loaded
            changed
        End Enum

#End Region



    End Class


End Namespace